Pro ASP.NET Core MVC2(第7版)翻译

第20章:API 控制器

作者:Adam Freeman

翻译:陈广

日期:2018-10-4


并不是所有的控制器都用于向客户端发送 HTML 文档。还有 API 控制器用于提供应用程序的数据。这是以前通过单独的Web API 框架提供的特性,但现在已经集成到 ASP.NET Core MVC 中。本章我将解释 API 控制器在 Web 应用程序中扮演的角色,描述它们解决的问题,并演示如何创建、测试和使用它们。表20-1为 API 控制器简述。

表 20-1:API 控制器简述

问题 回答
它们是什么? API 控制器与常规控制器类似,只不过它们的 action 方法产生的响应是发送到客户端的不包含 HTML 标记的数据对象。
它们有何用途? API 控制器允许客户端访问应用程序中的数据,而无需接收向用户呈现该内容所需的 HTML 标记。并非所有客户端都是浏览器,也不是所有客户端都向用户呈现数据。API 控制器使得应用程序开放了对新型客户端或使用第三方开发客户端的支持
如何使用它们 API 控制器与 HTML 控制器的使用类似
是否有任何缺陷或限制? 最常见的问题是涉及数据对象序列化的方式,以便将它们发送到客户端。有关详细信息,请参阅《理解内容格式》一节。
有没有其他选择? 您可以不在项目中使用 API 控制器,但是这样做可以增加平台对客户端的价值。

表20-2为本章摘要。

表20-2:本章摘要

问题 解决方案 清单
提供对应用程序中数据的访问 创建一个 API 控制器 10
从 API 控制器中请求数据 使用 Ajax 查询,直接使用浏览器的 API 或通过 jQuery 这样的库 11-13
重写内容协商过程 使用Produces特性 14-16
允许客户端通过在 URL 中指定数据格式来覆盖Accept header Startup类中添加格式化器映射,添加捕获数据格式的段变量,并可选择地应用FormatFilter特性。 17-18
为内容协商过程提供充分支持 启用HttpNotAcceptableOutputFormatter格式化器,并设置RespectBrowserAcceptHeader配置属性 19-20
使用不同的 action 方法以不同格式接收数据 应用Consumes特性 21

准备示例项目

本章我使用【ASP.NET Core Web 应用程序(.NET Core)】模板创建了一个名为 ApiControllers 的新的空项目。

创建模型和存储库

我首先创建了 Models 文件夹,添加了一个名为 Reservation.cs 的类文件,并使用它定义了清单20-1所示的模型类。

清单 20-1:Models 文件夹下的 Reservation.cs 文件的内容

namespace ApiControllers.Models
{
    public class Reservation
    {
        public int ReservationId { get; set; }
        public string ClientName { get; set; }
        public string Location { get; set; }
    }
}

我还向 Models 文件夹中添加了一个名为 IRepository.cs 的文件,并使用它来定义模型存储库的接口,如清单20-2所示。

清单 20-2:Models 文件夹下的 IRepository.cs 文件的内容

using System.Collections.Generic;

namespace ApiControllers.Models
{
    public interface IRepository
    {
        IEnumerable<Reservation> Reservations { get; }
        Reservation this[int id] { get; }

        Reservation AddReservation(Reservation reservation);
        Reservation UpdateReservation(Reservation reservation);
        void DeleteReservation(int id);
    }
}

我在 Models 文件夹中添加了一个名为 MemoryRepository.cs 的类文件,并使用它来定义IRepository接口的非持久性实现,如清单20-3所示。

清单 20-3:Models 文件夹下的 MemoryRepository.cs 文件的内容

using System.Collections.Generic;

namespace ApiControllers.Models
{
    public class MemoryRepository : IRepository
    {
        private Dictionary<int, Reservation> items;
        public MemoryRepository()
        {
            items = new Dictionary<int, Reservation>();
            new List<Reservation> {
                new Reservation { ClientName = "Alice", Location = "Board Room" },
                new Reservation { ClientName = "Bob", Location = "Lecture Hall" },
                new Reservation { ClientName = "Joe", Location = "Meeting Room 1" }
            }.ForEach(r => AddReservation(r));
        }

        public Reservation this[int id] => items.ContainsKey(id) ? items[id] : null;

        public IEnumerable<Reservation> Reservations => items.Values;

        public Reservation AddReservation(Reservation reservation)
        {
            if (reservation.ReservationId == 0)
            {
                int key = items.Count;
                while (items.ContainsKey(key)) { key++; };
                reservation.ReservationId = key;
            }
            items[reservation.ReservationId] = reservation;
            return reservation;
        }

        public void DeleteReservation(int id) => items.Remove(id);

        public Reservation UpdateReservation(Reservation reservation)
            => AddReservation(reservation);
    }
}

存储库在实例化时创建一组简单的模型对象,并且由于没有持久存储,所以当应用程序停止或重新启动时,任何更改都将丢失(有关如何创建持久存储库的示例,请参阅第8章,它是 SportsStore 示例应用程序的一部分)。

创建控制器和视图

在本章的后面,我将创建 RESTful 控制器,但在准备过程中,需要创建一个常规控制器,为后面的示例提供基础。我创建了 Controllers 文件夹,添加了一个名为 HomeController.cs 的文件,并使用它定义了如清单20-4所示的控制器。

清单 20-4:Controllers 文件夹下的 HomeController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using ApiControllers.Models;

namespace ApiControllers.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository { get; set; }

        public HomeController(IRepository repo) => repository = repo;

        public ViewResult Index() => View(repository.Reservations);

        [HttpPost]
        public IActionResult AddReservation(Reservation reservation)
        {
            repository.AddReservation(reservation);
            return RedirectToAction("Index");
        }
    }
}

此控制器定义Index action,这是应用程序的默认 action,并渲染数据模型。它还定义了一个AddReservation action,仅能用于 HTTP POST 请求,并将用于接收来自用户的表单数据。这些操作遵循第17章中描述的 Post/Redirect/Get 模式,以便重新加载网页不会创建重复的表单提交。

我创建了一个布局,以便将 HTML 内容与文档 header 分离开来,这将简化我在本章后面所做的一些更改。我创建了 Views/Shared 文件夹,添加了一个名为 _Layout.cshtml 的布局文件,并添加了清单20-5所示的标记。

清单 20-5:Views/Shared 文件夹下的 _Layout.cshtml 文件的内容

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>RESTful Controllers</title>
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    @RenderBody()
</body>
</html>

接下来,我创建了 Views/Home 文件夹,添加了一个名为 Index.cshtml 的视图文件,并添加了清单20-6所示的内容。

清单 20-6:Views/Home 文件夹下的 Index.cshtml 文件的内容

@model IEnumerable<Reservation>
@{ Layout = "_Layout"; }

<form id="addform" asp-action="AddReservation" method="post">
    <div class="form-group">
        <label for="ClientName">Name:</label>
        <input class="form-control" name="ClientName" />
    </div>
    <div class="form-group">
        <label for="Location">Location:</label>
        <input class="form-control" name="Location" />
    </div>
    <div class="text-center panel-body">
        <button type="submit" class="btn btn-sm btn-primary">Add</button>
    </div>
</form>

<table class="table table-sm table-striped table-bordered m-2">
    <thead><tr><th>ID</th><th>Client</th><th>Location</th></tr></thead>
    <tbody>
        @foreach (var r in Model)
        {
            <tr>
                <td>@r.ReservationId</td>
                <td>@r.ClientName</td>
                <td>@r.Location</td>
            </tr>
        }
    </tbody>
</table>

此强类型视图接收一系列Reservation对象作为它的模型,并使用 Razor foreach循环来填充表。还有一个表单已被配置为将 POST 请求发送到AddReservation action。

本章中的示例依赖于 Bootstrap CSS 包。若要将 Bootstrap 添加到项目中,我在 ApiControllers 项目中单击鼠标右键,在弹出菜单中选择【添加】➤【添加客户端库】,并将 twitter-bootstrap 添加至项目中。最终生成的 libman.json 配置文件代码清单18-7所示:

清单 20-7:ApiControllers 文件夹下的 libman.json 文件的内容

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

接下来,我在 Views 文件夹中创建了一个 _ViewImports.cshtml 文件,并使用它来设置内置标签助手,以便在 Razor 视图中使用,并导入模型命名空间,如清单20-8所示。

清单 20-8:Views 文件夹下的 _ViewImports.cshtml 文件的内容

@using ApiControllers.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

为了启用 MVC 框架和开发所需的中间件组件,我对Startup类进行了清单20-9所示的更改。我还使用AddSingleton方法为模型存储库设置服务映射。

清单 20-9:ApiControllers 文件夹下的 Startup.cs 文件,启用中间件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ApiControllers.Models;

namespace ApiControllers
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IRepository, MemoryRepository>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

设置 HTTP 端口

本章中的一些示例是通过手动键入 URL 来测试的。为了便于描述,我将设置用于接收 HTTP 请求的端口。从 Visual Studio 【项目】菜单中选择【ApiControllers 属性】,选中【调试】选项卡,并将【应用 URL】项的值更改为:http://localhost:7000,如图20-1所示。请确保在设置端口号后保存更改。

图20-1 设置应用程序 URL

启动应用程序,填写表单,然后单击【Add】按钮;应用程序将向模型添加一个新的Reservation,如图20-2所示。您对存储库所做的更改不是持久的,将在应用程序停止或重新启动时丢失。

图20-2 使用示例应用程序

理解 RESTful 控制器的角色

示例应用程序是一个典型的 web 应用程序的例子。应用程序中的所有逻辑都存在于服务器上,包含在 C# 类中,这使得它们易于管理、测试及维护。但以这种方式设计的应用程序在速度、效率和开放性方面可能存在严重缺陷。

理解速度问题

目前,示例应用程序是一个同步 Web 应用程序。当用户单击【Add】按钮时,浏览器将 POST 请求发送到服务器,等待响应,然后呈现它收到的 HTML。在此期间,用户只能等待,无法做任何事情。在开发过程中,当浏览器和服务器位于同一台机器上时,等待时间是不可察觉的;然而,已部署的应用程序受实际容量限制和延迟影响,同步应用程序要求用户等待响应的时间可能很长。

同步应用程序并不总是存在速度问题。例如,如果您正在编写业务应用程序,以便在所有客户端都通过快速可靠的局域网连接的单一位置使用,那么您可能没有需要解决的问题。另一方面,如果您正在为基础设施薄弱的地区的移动客户端编写应用程序,那么速度问题可能很严重。

提示:有些浏览器允许模拟不同类型的网络,这可能是一个有用的工具,可以查看用户是否可能接受在一系列场景中使用同步应用程序。例如,Google Chrome 提供了一个名为网络节流的功能,可以在 F12 开发工具的网络部分获得。有一系列预定义的网络可用,或者您可以通过指定上传和下载速率以及延迟来创建自己的网络。

理解效率问题

效率问题源于同步 Web 应用程序将浏览器视为 HTML 渲染引擎,仅用于显示服务器发送的 HTML 文档。

例如,当用户第一次请求示例应用程序的默认 URL 时,发送回的 HTML 文档包含浏览器显示应用程序 content 所需的所有内容,包括以下信息:

  • content 所依赖的 Bootstrap CSS 文件,如果缓存副本不可用,则需要下载该文件。
  • content 包含一个表单,该表单被配置为将 POST 请求发送到AddReservation action。
  • content 包含一个表,其 body 包含三个填充行。

示例应用程序很简单,初始请求导致服务器向客户端发送大约 1.3KB 的数据。但是,当用户提交表单时,客户端将再次被重定向到索引操作,这将导致另一个 1.3KB 的数据,以反映单个表行的添加。浏览器已经呈现了表单和表,但它们被丢弃,取而代之的是对基本相同内容的全新表示形式。

你可能认为 1.3KB 的数据并不多,当然,你是对的。但是,如果考虑到有用内容与重复内容的比率,就会发现发送到浏览器的绝大多数数据都是浪费的。示例应用程序是故意写得很简单的;真正的应用程序很少只需要如此少的 HTML,并且随着应用程序复杂性的增加,重复内容的数量将大大增加。

理解开放性问题

传统 Web 应用程序的最后一个问题是设计是封闭的,这意味着模型中的数据只能通过 Home 控制器提供的 action 来访问。当需要在另一个应用程序中使用底层数据时,封闭的应用程序就会成为一个问题,特别是当应用程序是由不同的团队甚至是不同的组织开发时。开发人员通常认为,应用程序的价值在于它为用户提供的用户交互,这很大程度上是因为这些是我们花时间思考和编写的部分。但是,一旦建立了一个应用程序并拥有了一个活跃的用户群,应用程序包含的数据通常就变得重要了。

引入 REST 和 API 控制器

API 控制器是一个 MVC 控制器,它负责提供对应用程序中数据的访问,而不将其封装在 HTML 中。这允许检索或修改模型中的数据,而不必使用常规控制器提供的 action,如示例应用程序中的 Home 控制器。

从应用程序中传递数据的最常用方法是使用*具象状态传输(Representational State Transfer)*模式,称为 REST。REST 没有详细的规范,这导致了许多不同的方法落在 RESTful 的旗帜之下。然而,在客户端 web 应用程序开发中,有一些统一的思想是有用的。

RESTful web 服务的核心前提是接受 HTTP 的特性,以便请求方法(也称为动词)指定服务器要执行的操作,请求 URL 指定操作将使用的一个或多个数据对象。

例如,下面是一个 URL,它可能引用示例应用程序中的指定的Reservation

/api/reservations/1

URL 的第一部分【api】用于将应用程序的数据部分从生成 HTML 的标准控制器中分离出来。下一部分【reservations】指示将在其上操作的对象的集合。最后一个部分【1】指定reservations集合中的单个对象。在示例应用程序中,唯一标识对象并将在 URL 中使用的是ReservationId属性的值。

标识对象的 URL 与 HTTP 方法相结合以指定操作。在表20-3中,我列出了最常见的 HTTP 方法以及它们与示例 URL 结合时所代表的内容。我还列出了有哪些数据(有效载荷)包含在每个方法和 URL 组合的请求和响应中。处理这些请求的API 控制器使用响应状态码报告请求的结果。

表 20-3:结合 HTTP 方法和 URL 来指定 RESTful Web 服务

动词 URL 描述 有效载荷
GET /api/reservations 此组合检索所有对象 响应包含Reservation对象的完整集合
GET /api/reservations/1 此组合检索其ReservationId为 1 的Reservation对象 响应包含指定的Reservation对象
POST /api/reservation 此组合创建一个新的Reservation 请求包含创建Reservation对象所需的其他属性的值。响应包含存储的对象,确保客户端接收保存的数据。
PUT /api/reservation 此组合替换了现有的Reservation 请求包含更改指定Reservation的属性所需的值。响应包含存储的对象,确保客户端接收保存的数据。
PATCH /api/reservation/1 此组合修改现有的Reservation对象,其ReservationId为1。 此请求包含一组应用于指定Reservation对象的修改。此响应确认已应用了更改。
DELETE /api/reservation/1 此组合删除ReservationId为1的Reservation对象 请求或响应中没有有效载荷

并非一定要遵循 RESTful 约定,但它确实有助于使您的应用程序更容易使用,因为许多已建立的 Web 应用程序都采用了这个相同的广泛使用的方法。

创建一个 API 控制器

创建 API 控制器的过程建立在标准控制器使用方法的基础上,并提供了一些其他特性来帮助指定呈现给客户端的 API。为了演示,我在 Controllers 文件夹中添加了一个名为 ReservationController.cs 的类文件,并使用它来定义清单20-10所示的类。在下面的部分中,我详细分析了这个控制器提供的功能。

提示:记住,控制器类可以定义在项目的任何地方,而不仅仅是在 Controllers 文件夹中。对于大型和复杂的项目,将 API 控制器与常规 HTML 控制器分开定义并将它们放置在子文件夹甚至完全独立的文件夹中是有帮助的。

清单 20-10:Controllers 文件夹下的 ReservationController.cs 文件的内容

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using ApiControllers.Models;
using Microsoft.AspNetCore.JsonPatch;

namespace ApiControllers.Controllers
{
    [Route("api/[controller]")]
    public class ReservationController : Controller
    {
        private IRepository repository;

        public ReservationController(IRepository repo) => repository = repo;

        [HttpGet]
        public IEnumerable<Reservation> Get() => repository.Reservations;

        [HttpGet("{id}")]
        public Reservation Get(int id) => repository[id];

        [HttpPost]
        public Reservation Post([FromBody] Reservation res) =>
            repository.AddReservation(new Reservation
            {
                ClientName = res.ClientName,
                Location = res.Location
            });

        [HttpPut]
        public Reservation Put([FromBody] Reservation res) =>
            repository.UpdateReservation(res);

        [HttpPatch("{id}")]
        public StatusCodeResult Patch(int id,
            [FromBody]JsonPatchDocument<Reservation> patch)
        {
            Reservation res = Get(id);
            if (res != null)
            {
                patch.ApplyTo(res);
                return Ok();
            }
            return NotFound();
        }

        [HttpDelete("{id}")]
        public void Delete(int id) => repository.DeleteReservation(id);
    }
}

API 控制器与常规控制器工作的基本方式相同,这意味着您可以创建 POCO 控制器或从Controller基类派生类,从而提供对请求 context 数据的更方便的访问。


调整 RESTful 模式

REST 在 web 应用程序 API 应该如何呈现给客户端方面鼓励了一定程度的教条主义。REST 不是一个标准的,甚至不是一个定义良好的模式,而且有一些有用的方法可以使 RES T更容易在 ASP.NET Core MVC 应用程序中采用,但是有一种倾向使那些对 RESTful 有固定观点的程序员感到不安。

在表20-3中,我为 POST 和 PUT 操作列出的 URL 并不唯一地标识资源,有些人认为这是一个基本的 REST 特性。在 POST 操作中,Reservation对象的唯一标识符由模型分配,这意味着客户端无法将其作为 URL 的一部分提供。在 PUT 操作中,MVC 模型绑定特性 —— 我在第26章中描述了它,也是我在清单20-10中应用FromBody特性的原因 —— 使您更容易从请求体接收要修改的Reservation对象的详细信息。因此,Reservation控制器希望在那里找到ReservationId值,以标识要修改的模型对象。

与所有模式一样,REST 是一个包含有用的想法的起点。这不是一个必须不惜一切代价遵守的严格标准,唯一重要的是编写能够被理解、测试和维护的代码。考虑到 MVC 应用程序的特性和存储库的设计,使得应用程序更加简单,同时仍然为客户端提供了一个有用的 API。我的建议是,将模式视为一项指导原则,使您能够适应自己的需求 —— 这对于 REST 和 MVC 本身都是一样的。


定义路由

到达 API 控制器的路由只能使用Route特性定义,不能在Startup类中的应用程序配置中定义。API 控制器的约定是使用一个以 api 为前缀的路由,后面跟着控制器的名称,这样就可以通过URL /api/reservation 达到清单20-10所示的ReservationController控制器,如下所示:

...
[Route("api/[controller]")]
public class ReservationController : Controller {
...

声明依赖项

API 控制器的实例化方式与常规控制器相同,这意味着它们可以声明将使用服务提供者解析的依赖项。ReservationController类声明了一个依赖于IRepository接口的构造函数,它将被解析为提供对模型中数据的访问。

...
public ReservationController(IRepository repo) => repository = repo;
...

定义 Action 方法

每个 action 方法都使用一个特性修饰,用于指定它接受的 HTTP 方法,如下所示:

...
[HttpGet]
public IEnumerable<Reservation> Get() => repository.Reservations;
...

HttpGet特性是用于将对 action 方法的访问限制为具有特定 HTTP 方法或动词的请求的集合之一。表20-4描述了完整的特性集。

表 20-4:HTTP 方法特性

名称 描述
HttpGet 此特性指定只能由使用 GET 动词的 HTTP 请求调用该 action
HttpPost 此特性指定只能由使用 POST 动词的 HTTP 请求调用该 action
HttpDelete 此特性指定只能由使用 DELETE 动词的 HTTP 请求调用该 action
HttpPut 此特性指定只能由使用 PUT 动词的 HTTP 请求调用该 action
HttpPatch 此特性指定只能由使用 PATCH 动词的 HTTP 请求调用该 action
HttpHead 此特性指定只能由使用 HEAD 动词的 HTTP 请求调用该 action
AcceptVerbs 此特性用于指定多个 HTTP 动词

通过将路由片段作为 HTTP 方法特性的参数进行进一步细化,如下所示:

...
[HttpGet("{id}")]
public Reservation Get(int id) => repository[id];
...

路由片段{id}与应用于控制器的Route特性定义的路由和基于 HTTP 方法的约束相结合。在这种情况下,这意味着可以通过发送一个 GET 请求来实现此操作,该请求的 URL 与 /api/reservations/{id} 路由模式相匹配,其中id段随后用于标识应该检索的Reservation对象。

注意,为 API 控制器生成的路由不包括{action}段变量,这意味着 action 方法的名称不是针对指定方法所需的 URL 的一部分。API 控制器中的所有 action 都是通过相同的基本 URL(如 /api/reservation)实现的,并且使用 HTTP 方法和可选段来区分它们。

定义 Action Results

API 控制器的 action 方法不依赖于ViewResult对象来表示其结果,因为在传递数据时不需要视图。相反,API 控制器 action 方法返回数据对象,如下所示:

...
[HttpGet]
public IEnumerable<Reservation> Get() => repository.Reservations;
...

此 action 返回一系列Reservation对象,并让 MVC 负责将它们序列化为可由客户端处理的格式。我在《理解内容格式》一节中更详细地解释了这个过程。


自定义 API 结果

API 控制器最吸引人的地方之一是您可以在 action 方法中返回 C# 对象,并让 MVC 指出如何处理它们。MVC 非常擅长于解决该做什么。例如,如果从 API 控制器 action 方法返回null,则客户端将被发送【 204 – No Content】响应。

但是 API 控制器也可以使用常规控制器可使用的特性,这意味着您可以通过从 action 方法返回指定要发送的结果类型的IActionResult来覆盖默认行为。例如,下面是一个来自示例控制器的 action 方法的实现,该方法在查询与模型中的对象不匹配时发送【404 – Not Found】响应:

...
[HttpGet("{id}")]
public IActionResult Get(int id) {
    Reservation result = repository[id];
    if (result == null) {
        return NotFound();
    } else {
        return Ok(result);
    }
}
...

如果存储库中没有指定 ID 的对象,则调用NotFound方法,该方法创建一个NotFoundResult对象,它将一个【404 – Not Found】响应发送到客户端。如果存储库中有一个对象,则调用Ok方法来创建ObjectResult对象。Ok方法允许我在返回IActionResult的 action 中向客户端发送一个对象,如第17章所述。您通常不需要覆盖默认的 API 控制器响应,但是如果需要的话,所有的 action results 都是可用的。


测试 API 控制器

有很多工具可以帮助测试 Web 应用程序 API。例如 Fiddlerwww.telerik.com/fiddler),这是一个独立的 HTTP 调试工具,以及 Swashbuckle(http://github.com/domaindrivendev/Swashbuckle),这是一个 NuGet 包,它将一个摘要页添加到一个应用程序中,并描述了它的 API 操作并允许对它们进行测试。

但是确保 API 控制器的最简单的方法是使用 PowerShell,它可以轻松地从命令行创建 HTTP 请求,并且可以让您聚焦于 API 操作的结果,而无需深入了解细节。PowerShell 起源于 Windows,但现在也可用于 Linux 和 macOS。

在下面的部分中,我将向您展示如何使用 PowerShell 测试 Reservation 控制器提供的每个操作。您可以打开一个新的 PowerShell 窗口来运行测试命令,或者使用 Visual Studio 的【程序包管理器控制台】窗口。

测试 GET 操作

若要测试由 Reservation API 控制器提供的 GET 操作,请从 Visual Studio 【调试】菜单中选择【开始执行(不调试)】来启动应用程序,然后等待,直到您看到 Home 控制器提供的同步响应。运行应用程序后,打开 PowerShell 窗口并键入以下命令:

Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

此命令使用Invoke-RestMethod PowerShell cmdlet 向 /api/reservation URL 发送 GET 请求。对结果进行分析和格式化,使数据易于读取,如下所示:

reservationId clientName location
------------- ---------- --------
            0 Alice      Board Room
            1 Bob        Lecture Hall
            2            Joe Meeting Room 1

服务器通过模型中包含的Reservation对象的 JSON 表示来响应 GET 请求,Invoke-RestMethod cmdlet 以表格格式表示该对象。


理解 JSON

JavaScript Object Notation (JSON) 已成为 Web 应用程序的标准数据格式。JSON 之所以流行,是因为它简单、简洁、易于使用。使用 Javascript 代码处理 JSON 数据特别简单,因为 JSON 格式类似于在 Javascript 代码中表示字面量对象的方式。现代浏览器包含了对生成和解析 JSON 数据的内置支持,流行的 Javascript 库(如 jQuery)将自动与 JSON 进行转换。尽管 JSON 是从 Javascript 发展而来的,但它的结构对于 C# 开发者来说很容易读取和使用。例如,下面是示例应用程序中 API 控制器的响应:

...
[{"reservationId":0,"clientName":"Alice","location":"Board Room"},
{"reservationId":1,"clientName":"Bob","location":"Lecture Hall"},
{"reservationId":2,"clientName":"Joe","location":"Meeting Room 1"}]
...

这个 JSON 字符串描述了一个对象数组。数组由[]字符包含,每个对象使用{}字符包含。对象是 键/值 对的集合,其中每个键与其值用冒号:分隔,每一对用逗号,分隔。这与我在MemoryRepository类中用于定义清单20-3中数据的 C# 字面量语法大致类似。

...
new List<Reservation> {
    new Reservation { ClientName = "Alice", Location = "Board Room" },
    new Reservation { ClientName = "Bob", Location = "Lecture Hall" },
    new Reservation { ClientName = "Joe", Location = "Meeting Room 1" }
...

但是,请注意,MVC 将属性名称首字母从 C# 约定(ClientName,首字母大写)更改为 Javascript 约定(clientName,首字母小写)。

尽管格式并不相同,但也有足够的相似之处让 C# 开发人员能够轻松地读取和理解 JSON 数据。对于大多数 web 应用程序,您不需要深入了解 JSON 的细节,因为 MVC 完成了所有重要的任务,但是您可以在www.json.org上了解更多关于 JSON 的信息。


Reservation 控制器提供两个 GET 操作。当 GET 请求被发送到 /api/reservation 时,将返回一个包含所有对象的响应。要检索单个对象,它的ReservationId值指定为 URL 中的最后一个段,如下所示:

Invoke-RestMethod http://localhost:7000/api/reservation/1 -Method GET

此命令请求Reservation对象,其ReservationId值为1,并产生以下结果:

reservationId clientName location
------------- ---------- --------
            1 Bob        Lecture Hall

测试 POST 操作

API 控制器提供的所有操作都可以使用 PowerShell 进行测试,尽管命令的格式可能有点笨拙。下面是一个命令,它向API 控制器发送 POST 请求,以便在存储库中创建一个新的Reservation对象,并写出响应中发送的数据:

Invoke-RestMethod http://localhost:7000/api/reservation -Method POST -Body (@{clientName="Anne"; location="Meeting Room 4"} | ConvertTo-Json) -ContentType "application/json"

此命令使用-Body参数指定请求的 body,该请求编码为 JSON。-ContentType参数用于设置请求的Content-Type header。该命令将产生以下结果:

reservationId clientName location
------------- ---------- --------
            3 Anne       Meeting Room 4

POST 操作使用clientNameLocation值创建Reservation对象,并将新对象的 JSON 表示形式返回给客户端,其中包括已分配给新对象的ReservationId值。这可能看起来只是客户端接收它在请求中发送给服务器的数据值,但这种方法确保了客户端处理的数据与服务器使用的数据相同,并满足服务器对从客户端接收到的数据执行的任何格式设置或转换。要查看 POST 请求的效果,将另一个 GET 请求发送到 /api/reservation API,如下所示:

Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

客户端返回的数据反映了新Reservation对象的添加。

reservationId clientName location
------------- ---------- --------
            0 Alice      Board Room
            1 Bob        Lecture Hall
            2 Joe        Meeting Room 1
            3 Anne       Meeting Room 4

测试 PUT 操作

使用 PUT 方法代替模型中现有的对象。对象的ReservationId值指定为请求 URL 的一部分,clientNameLocation值在请求 body 中提供。下面是一个 PowerShell 命令,它发送 PUT 请求来修改 Reservation 对象:

Invoke-RestMethod http://localhost:7000/api/reservation -Method PUT -Body (@{reservationId="1"; clientName="Bob"; location="Media Room"} | ConvertTo-Json) -ContentType "application/json"

此请求更改了Reservation对象,其ReservationId值为1,并为Location属性指定了一个新值。如果运行该命令,您将看到以下响应,这表明已经进行了更改:

reservationId clientName location
------------- ---------- --------
            1 Bob        Media Room

要查看 PUT 请求的效果,将 GET 请求发送到 /api/reservation API,如下所示:

Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

客户端返回的数据反映了新Reservation对象的添加。

reservationId clientName location
------------- ---------- --------
            0 Alice      Board Room
            1 Bob        Media Room
            2 Joe        Meeting Room 1
            3 Anne       Meeting Room 4

测试 Patch 操作

PATCH 方法用于修改模型中的现有对象。许多应用程序使用 PUT 请求并完全忽略 PATCH,如果客户端能够访问模型中的对象定义的所有属性,这是一种合理的方法。但是在复杂的应用程序中,由于安全原因,客户端可能会收到一组特定的属性值,这阻碍了它们成为 PUT 请求的一部分以发送完整的对象。PATCH 请求更有选择性,允许客户端为对象指定一个可更改粒度的集合。

ASP.NET Core MVC 已支持使用 JSON Patch 标准,该标准允许以统一的方式指定更改。我不打算详细介绍 JSON Patch 标准,您可以在 https://tools.ietf.org/html/rfc6902 上阅读该标准,但是对于示例应用程序,客户端将在其 HTTP PATCH 请求中发送如下 API 控制器 JSON 数据:

[
    { "op": "replace", "path": "clientName", "value": "Bob"},
    { "op": "replace", "path": "location", "value": "Lecture Hall"}
]

JSON Patch 文档表示为操作数组。每个操作都有一个op属性(它指定操作的类型)和一个path属性(它指定将在何处应用该操作)。

对于示例应用程序 —— 实际上,对于大多数应用程序 —— 只需要 replace 操作,该操作用于更改属性值。这个 JSON Patch 数据为clientNameLocation属性设置新的值,而要修改的对象将由请求 URL 标识。ASP.NET Core MVC 将自动处理 JSON 数据并将其作为JsonPatchDocument<T>对象呈现给 action 方法,其中T是要修改的模型对象的类型。然后JsonPatchDocument<T>对象使用ApplyTo方法更改来自存储库中的对象。下面是发送 PATCH 请求的 PowerShell 命令:

Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method PATCH -Body (@{ op="replace"; path="clientName"; value="Bob"},@{ op="replace"; path="location";value="Lecture Hall"} | ConvertTo-Json) -ContentType "application/json"

此请求要求服务器修改Reservation对象的clientNameLocation属性,该对象的ReservationId为2。要查看 PUT 请求的效果,将 GET 请求发送到 /api/reservation API,如下所示:

Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

客户端返回的数据反映了新Reservation对象的添加。

reservationId clientName location
------------- ---------- --------
            0 Alice      Board Room
            1 Bob        Media Room
            2 Bob        Lecture Hall
            3 Anne       Meeting Room 4

测试 Delete 操作

最后的测试是发送一个 DELETE 请求,该请求将从存储库中删除一个Reservation对象,如下所示:

Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method DELETE

Reservation 控制器中接受 DELETE 请求的 action 不会返回结果,因此在命令完成后不会显示任何数据。要查看删除的效果,请使用以下命令请求存储库的内容:

Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

从存储库中删除了Reservation对象,其ReservationId值为2。

reservationId clientName location
------------- ---------- --------
            0 Alice      Board Room
            1 Bob        Media Room
            3 Anne       Meeting Room 4

在浏览器中使用 API 控制器

定义 API 控制器已经解决了我的应用程序的开放性问题,但它对我的速度和效率问题没有任何帮助。为此,我需要更新应用程序的 HTML 部分,以便它依赖 JavaScript 向 API 控制器发出 HTTP 请求以执行数据操作。更广泛地说,本节中描述的技术是单页应用程序的基础,其中使用单个 HTML 页面中的 JavaScript 为应用程序的多个部分提取数据,生成要动态显示的内容。

注意:客户端开发本身就是一个主题,超出了本书的范围。在本节中,我只创建一个基本的异步 HTTP 请求,而没有详细解释,只是为了了解一下它是如何完成的。请参阅 Apress 出版的我写的【Pro Angular and Essential Angular for ASP.NET Core MVC】这本书,以获得关于使用 Angular 框架进行客户端开发和使用 ASP.NET Core MVC 支持 Angular 客户端的详细介绍。

浏览器为发出 Ajax 请求提供了JavaScript API,但是处理起来有点麻烦,浏览器实现一些可选功能的方式也有一些不同。发出 Ajax 请求的最简单方法是使用 jQuery 库,jQuery 库对于客户端开发来说是一个非常有用的工具。在清单20-11中,我将 jQuery 包添加到 libman.json 文件中。

清单 20-11:ApiControllers 文件夹下的 libman.json 文件,添加 jQuery

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    },
    {
      "library": "jquery@3.3.1",
      "destination": "wwwroot/lib/jquery/"
    }
  ]
}

事实上,由于一些 Bootstrap 特性依赖于 jQuery,LibMan 已经在 wwwroot/lib 文件夹中安装了该包。清单20-11中的添加具有显式依赖的效果。为了使用 jQuery 提供的特性,我创建了 wwwroot/js 文件夹,并添加了一个名为 client.js 的 JavaScript 文件,其内容如清单20-12所示。

清单 20-12:wwwroot/js 文件夹下的 client.js 文件的内容

$(document).ready(function () {
    $("form").submit(function (e) {
        e.preventDefault();
        $.ajax({
            url: "api/reservation",
            contentType: "application/json",
            method: "POST",
            data: JSON.stringify({
                clientName: this.elements["ClientName"].value,
                location: this.elements["Location"].value
            }),
            success: function (data) {
                addTableRow(data);
            }
        });
    });
});

var addTableRow = function (reservation) {
    $("table tbody").append("<tr><td>" + reservation.reservationId + "</td><td>"
        + reservation.clientName + "</td><td>"
        + reservation.location + "</td></tr>");
}

当用户在浏览器中提交表单时,该文件中的 JavaScript 文件响应,将表单数据编码为 JSON,并使用 HTTP POST 请求将其发送到服务器。jQuery自动解析服务器返回的 JSON 数据,然后将该行添加到 HTML 表格中。在清单20-13中,我更新了布局,以包含 client.js 文件的 jQuery 库的脚本元素。

清单 20-13:_Layout.cshtml 文件,添加 JavaScript 引用

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>RESTful Controllers</title>
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
    <script src="lib/jquery/jquery.js"></script>
    <script src="js/client.js"></script>
</head>
<body class="m-1 p-1">
    @RenderBody()
</body>
</html>

第一个script元素告诉浏览器加载 jQuery 库,第二个script素将包含我的自定义代码的文件。如果运行应用程序并使用 HTML 表单在应用程序存储库中创建Reservation,则没有明显的视觉差异,但是如果检查浏览器发送的 HTTP 请求,则会发现它所需的数据比应用程序的同步版本少得多。在我的简单测试中,异步请求需要 440 字节的数据,大约是同步请求所需的 40%。这一改进在实际应用程序中更为显著,在实际应用程序中,数据的大小往往比用于显示它的 HTML 文档小得多。

理解内容格式

当 action 方法返回一个 C# 对象作为它的结果时,MVC 必须确定应该使用哪种数据格式来编码对象并将其发送到客户端。在本节中,我将解释默认进程是什么,以及它如何受到客户端发送的请求和应用程序的配置的影响。为了帮助解释流程是如何工作的,我在 Controllers 文件夹中添加了一个名为 ContentController.cs 的类文件,并使用它定义了清单20-14所示的 API 控制器。

清单 20-14:Controllers 文件夹下的 ContentController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using ApiControllers.Models;

namespace ApiControllers.Controllers
{
    [Route("api/[controller]")]
    public class ContentController : Controller
    {
        [HttpGet("string")]
        public string GetString() => "This is a string response";

        [HttpGet("object")]
        public Reservation GetObject() => new Reservation
        {
            ReservationId = 100,
            ClientName = "Joe",
            Location = "Board Room"
        };
    }
}

我将静态段变量指定为此控制器中两个 action 的HttpGet特性的参数,这意味着它们可以由 /api/controller/string 和 /api/controller/object URL 来实现。Content 控制器甚至不会松散地遵循 REST 模式,但它将使您更容易理解内容协商的工作方式。

MVC 选择的内容格式取决于四个因素:客户端将接受的格式、MVC 可以生成的格式、action 指定的内容策略以及 action 方法返回的类型。弄清楚所有东西是如何组合在一起的可能会令人望而生畏,但好消息是,默认策略对于大多数应用程序来说已经很好,而且您只需要了解当您需要进行更改或没有按照您期望的格式获得结果时在幕后发生的事情。

理解默认内容策略

起点是标准的应用程序配置,当客户端和 action 方法都不对可以使用的格式应用任何限制时,就会使用该配置。在这种情况下,输出是简单和可预测的。

  • 如果 action 方法返回一个字符串,则字符串原封不动地发给客户端,并且响应的Content-Type header 被设置为text/plain
  • 对于所有其他数据类型,包括其他简单类型(如int),数据被格式化为 JSON,响应的Content-Type header 被设置为application/json

字符串得到特殊处理的原因是,当字符串被编码为 JSON 时会引起问题。当对其它的简单类型进行编码时,如 C# 的int值 2,结果是一个用引号括起来的字符串,如"2"。当对字符串编码时,你会得到两组引号,这样"Hello"就变成了""Hello""。并不是所有的客户端都能很好地处理这种双重编码,所以使用 text/plain 格式并完全避开这个问题是更可靠的。这不太会成为一个问题,因为很少有应用程序发送字符串值;以 JSON 格式发送对象更为常见。通过使用 PowerShell,您可以看到这两种输出。下面是一个调用GetString方法的命令,该方法返回一个字符串:

Invoke-WebRequest http://localhost:7000/api/content/string | select @{n='Content-Type';e={ $_.Headers."Content-Type" }}, Content

该命令向 /api/content/string URL 发送一个 GET 请求,并处理响应,以显示来自响应的内容类型标头和内容。

提示:如果尚未为 Internet Explorer 执行初始设置,则使用Invoke-WebRequest cmdlet 时可能会收到错误。在 Windows 10 机器上,这种情况尤其可能发生,因为 Edge 已经取代了它。这个问题可以通过运行 IE 并选择所需的初始配置来解决。

命令产生以下输出:

Content-Type              Content
------------              -------
text/plain; charset=utf-8 This is a string response

同样的命令也可以通过更改请求的 URL 来显示 JSON 格式,如下所示:

Invoke-WebRequest http://localhost:7000/api/content/object | select @{n='Content-Type';e={ $_.Headers."Content-Type" }}, Content

此命令生成输出,为清晰起见进行了格式化,显示响应已编码为 JSON:

Content-Type                    Content
------------                    -------
application/json; charset=utf-8 {"reservationId":100,"clientName":"Joe","location":"Board Room"}

理解内容协商

大多数客户端将在请求中包含一个Accept header,该请求指定他们愿意在响应中接收的一组格式,表示为一组 MIME 类型。以下是Google Chrome在请求中发送的Accept header:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

这个 header 表示 Chrome 可以处理 HTML 和 XHTML 格式(XHTML 是与 XML 兼容的 HTML 方言)、XML 和 WEBP 图像格式。标头中的q值指定相对优先级,默认情况下该值为 1.0。将 application/xml 的q值指定为 0.9 会告诉服务器,Chrome 将接受 XML 数据,但更倾向于处理 HTML 或 XHTML。最后一个项*/*告诉服务器,Chrome 将接受任何格式,但它的q值指定它是指定类型中最低的优先级。将所有这些结合在一起意味着 Chrome 发送的Accept header 向服务器提供了以下信息:

  1. Chrome 更喜欢接收 HTML 或 XHTML 数据或 WEBP 图像。
  2. 如果这些格式不可用,那么下一个最首选的格式是 XML。
  3. 如果无首选格式可用,那么 Chrome 将接受任何格式。

由此,您可能会假设可以通过设置Accept header 来更改请求从 MVC 应用程序接收到的格式,但它不是这样工作的 —— 或者说,仅仅因为需要做一些准备,它还不能这样工作。首先,下面是一个 PowerShell 命令,它将一个 GET 请求发送到 GetObject 方法,其中包含一个Accept header,指定客户端将只接受 XML 数据:

Invoke-WebRequest http://localhost:7000/api/content/object -Headers @{Accept="application/xml"} | select @{n='Content-Type';e={ $_.Headers."Content-Type" }}, Content

以下是结果,显示服务器发送了一个 application/json 响应:

Content-Type                    Content
------------                    -------
application/json; charset=utf-8 {"reservationId":100,"clientName":"Joe","location":"Board Room"}

包含Accept header 对格式没有影响,即使服务器已经向客户端发送了未指定的格式。问题是,在默认情况下,MVC 被配置为只支持 JSON,因此它没有其他可以使用的格式。MVC 发送 JSON 数据不是返回错误,而是希望客户机能够处理它,即使它不是请求Accept header 指定的格式之一。


配置 JSON 序列化程序

ASP.NET Core MVC 使用流行的第三方 JSON 包 Json.net 将对象序列化为 JSON。默认配置适用于大多数项目,但如果需要以特定方式创建 JSON,则可以更改。AddMvc().AddJsonOptions扩展方法用于Startup类,并提供对MvcJsonOptions对象的访问,通过该对象配置Json.net包。有关配置选项的详细信息,请参见www.newtonsoft.com/json


启用 XML 格式

要在工作中看到内容协商,您必须给 MVC 一些格式的选择,它用于编码响应数据。尽管 JSON 已经成为 web 应用程序的默认格式,但是 MVC 也可以支持将数据编码为 XML,如清单20-15所示。

提示:您可以通过Microsoft.AspNetCore.Mvc.Formatters.OutputFormatter类创建自己的内容格式。这很少被使用,因为创建自定义数据格式并不是在应用程序中公开数据的有用方法,而且最常见的格式 —— JSON 和 XML 已经实现。

清单 20-15:ApiControllers 文件夹下的 Startup.cs 文件,启用 XML 格式

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ApiControllers.Models;

namespace ApiControllers
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IRepository, MemoryRepository>();
            services.AddMvc().AddXmlDataContractSerializerFormatters();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

当 MVC 只有 JSON 格式可用时,它别无选择,只能将响应编码为 JSON。现在有了选择,您可以看到内容协商过程更加充分。

提示:我在清单20-15中使用了AddXmlDataContractSerializerFormatters扩展方法,但您也可以使用AddXmlSerializerFormatters扩展方法,它提供了对旧序列化类的访问。如果要为较老的 .NET 客户端生成 XML 内容,这种差异可能会有所帮助。

下面是再次请求 XML 数据的 PowerShell 命令:

Invoke-WebRequest http://localhost:7000/api/content/object -Headers @{Accept="application/xml"} | select @{n='Content-Type';e={ $_.Headers."Content-Type" }}, Content

运行此命令,您将看到服务器现在返回 XML 数据,而不是 JSON,如下所示(为了简洁起见,我省略了XML命名空间属性):

Content-Type                    Content
------------                    -------
application/xml; charset=utf-8  <Reservation>
                                    <ClientName>Joe</ClientName>
                                    <Location>Board Room</Location>
                                    <ReservationId>100</ReservationId>
                                </Reservation>

指定 action 数据格式

您可以通过应用Products特性来覆盖内容协商系统,并在操作方法上直接指定数据格式,如清单20-16所示。

清单 20-16:Controllers 文件夹下的 ContentController.cs 文件,指定数据格式

using Microsoft.AspNetCore.Mvc;
using ApiControllers.Models;

namespace ApiControllers.Controllers
{
    [Route("api/[controller]")]
    public class ContentController : Controller
    {
        [HttpGet("string")]
        public string GetString() => "This is a string response";

        [HttpGet("object")]
        [Produces("application/json")]
        public Reservation GetObject() => new Reservation
        {
            ReservationId = 100,
            ClientName = "Joe",
            Location = "Board Room"
        };
    }
}

Products特性是一个过滤器,它更改ObjectResult对象的内容类型,MVC 在幕后使用它们来表示 API 控制器中的 action results。特性的参数指定将用于 action 结果的格式,还可以指定其他允许的类型。Products特性强制使用响应所使用的格式,运行以下 PowerShell 命令可以看到该格式:

(Invoke-WebRequest http://localhost:7000/api/content/object -Headers @{Accept="application/xml"}).Headers."Content-Type"

此命令显示到 /api/content/object URL 的 GET 请求所响应的Content-Type header 的值。运行该命令将显示正在使用 JSON,这是Produces特性指定的,尽管请求的Accept header 指定应该使用 XML。

从路由或查询字符串获取数据格式

Accept header 并不总是由编写客户端的程序员控制,特别是在使用旧浏览器或工具包进行开发的情况下。对于这种情况,允许通过用于目标 action 方法的路由或请求 URL 的查询字符串部分请求响应的数据格式可能会有所帮助。第一步是在Startup类中定义可用于引用路由或查询字符串中的格式的速记值。默认情况下有一个映射,其中json用作 application/json 的简写。在清单20-17中,我为 XML 添加了一个额外的映射。

清单 20-17:ApiControllers 文件夹下的 Startup.cs 文件,添加 格式速记

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ApiControllers.Models;
using Microsoft.Net.Http.Headers;

namespace ApiControllers
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IRepository, MemoryRepository>();
            services.AddMvc()
                .AddXmlDataContractSerializerFormatters()
                .AddMvcOptions(opts => {
                    opts.FormatterMappings.SetMediaTypeMappingForFormat("xml",
                        new MediaTypeHeaderValue("application/xml"));
                });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

MvcOptions.FormatterMappings属性用于设置和管理映射。在清单中,我使用SetMediaTypeMappingForFormat方法创建了一个新的映射,以便速记 XML 将引用 application/xml 格式。下一步是将FormatFilter特性应用于 action 方法,并可选择地调整 action 的路由,使其包含一个format段变量,如清单20-18所示。

清单 20-18:Controllers 文件夹下的 ContentController.cs 文件,应用 FormatFilter 特性

using Microsoft.AspNetCore.Mvc;
using ApiControllers.Models;

namespace ApiControllers.Controllers
{
    [Route("api/[controller]")]
    public class ContentController : Controller
    {
        [HttpGet("string")]
        public string GetString() => "This is a string response";

        [HttpGet("object/{format?}")]
        [FormatFilter]
        [Produces("application/json", "application/xml")]
        public Reservation GetObject() => new Reservation
        {
            ReservationId = 100,
            ClientName = "Joe",
            Location = "Board Room"
        };
    }
}

我已经将FormatFilter特性应用于GetObject方法,并修改了 action 的路由,以便它包含一个可选的format段。您不必将Produces特性与FormatFilter特性结合使用,但如果使用,则只有指定配置了Produces特性的格式的请求才能工作。指定Produces特性尚未配置的格式的请求将收到【404 – Not Found】的响应。如果不应用Produces特性,则请求可以指定 MVC 配置使用的任何格式。

我还向Produces特性添加了 application/xml 格式,以便 action 方法同时支持对 JSON 和 XML 的请求。

此 PowerShell 命令将 XML 格式指定为请求 URL 的一部分:

(Invoke-WebRequest http://localhost:7000/api/content/object/xml).Headers."Content-Type"

运行此命令将显示响应的内容类型,如下所示:

application/xml; charset=utf-8

FormatFilter特性查找名为format的路由段变量,获取它包含的速记值,并从应用程序配置中检索相关的数据格式。然后,此格式用于响应。如果没有可用的路由数据,则还将检查查询字符串。下面是使用查询字符串请求 XML 的 PowerShell 命令:

(Invoke-WebRequest http://localhost:7000/api/content/object?format=xml).Headers. "Content-Type"

FormatFilter特性找到的格式覆盖Accept header 指定的任何格式,这将格式选择权交给客户端开发人员,即使在使用不允许设置Accept header 的工具包和浏览器时也是如此。

启用全面内容协商

对于大多数应用程序来说,在没有其他可用格式的情况下发送 JSON 数据是一种明智的策略,因为 web 应用程序客户端更有可能错误地设置其接受头,而不是无法处理 JSON。也就是说,一些应用程序将不得不处理如果返回 JSON 会导致问题的客户端,而不管Accept header 说什么。要使内容协商正常工作,需要在Startup类中进行两个配置更改,如清单20-19所示。

清单 20-19:ApiControllers 文件夹下的 Startup.cs 文件,启用全面内容协商

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ApiControllers.Models;
using Microsoft.Net.Http.Headers;

namespace ApiControllers
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IRepository, MemoryRepository>();
            services.AddMvc()
            .AddXmlDataContractSerializerFormatters()
            .AddMvcOptions(opts => {
                opts.FormatterMappings.SetMediaTypeMappingForFormat("xml",
                    new MediaTypeHeaderValue("application/xml"));
                opts.RespectBrowserAcceptHeader = true;
                opts.ReturnHttpNotAcceptable = true;
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

使用RespectBrowserAcceptHeader选项来控制Accept header 是否得到充分尊重。如果没有合适的可用格式,则使用返回选项ReturnHttpNotAcceptable来控制是否将一个【406 - Not Acceptable】的响应发送到客户端。

我还必须从 action 方法中删除Produces特性,这样内容协商过程就不会被覆盖,如清单20-20所示。

清单 20-20:Controllers 文件夹下的 ContentController.cs 文件,移除 Produces 特性

using Microsoft.AspNetCore.Mvc;
using ApiControllers.Models;

namespace ApiControllers.Controllers
{
    [Route("api/[controller]")]
    public class ContentController : Controller
    {
        [HttpGet("string")]
        public string GetString() => "This is a string response";

        [HttpGet("object/{format?}")]
        [FormatFilter]
        //[Produces("application/json", "application/xml")]
        public Reservation GetObject() => new Reservation
        {
            ReservationId = 100,
            ClientName = "Joe",
            Location = "Board Room"
        };
    }
}

下面是一个 PowerShell 命令,它向 /api/content/object URL 发送 GET 请求,其中包含一个Accept header,指定应用程序无法提供的内容类型:

Invoke-WebRequest http://localhost:7000/api/content/object -Headers @{Accept="application/custom"}

如果运行此命令,将显示 406 错误消息,向客户端指示服务器无法提供所请求的格式。

接收不同的数据格式

当客户端向控制器发送数据时,例如在 POST 请求中,您可以通过使用Consumes特性来指定不同的 action 方法处理特定的数据格式,如清单20-21所示。

清单 20-21:Controllers 文件夹下的 ContentController.cs 文件,处理不同的数据格式

using Microsoft.AspNetCore.Mvc;
using ApiControllers.Models;

namespace ApiControllers.Controllers
{
    [Route("api/[controller]")]
    public class ContentController : Controller
    {
        [HttpGet("string")]
        public string GetString() => "This is a string response";

        [HttpGet("object/{format?}")]
        [FormatFilter]
        //[Produces("application/json", "application/xml")]
        public Reservation GetObject() => new Reservation
        {
            ReservationId = 100,
            ClientName = "Joe",
            Location = "Board Room"
        };

        [HttpPost]
        [Consumes("application/json")]
        public Reservation ReceiveJson([FromBody] Reservation reservation)
        {
            reservation.ClientName = "Json";
            return reservation;
        }

        [HttpPost]
        [Consumes("application/xml")]
        public Reservation ReceiveXml([FromBody] Reservation reservation)
        {
            reservation.ClientName = "Xml";
            return reservation;
        }
    }
}

ReceiveJsonReceiveXml action 都接受 POST 请求,它们之间的区别是使用Consumes特性指定的数据格式,它检查Content-Type header,以确定 action 方法是否可以处理请求。结果是,当有一个请求的Content-Type设置为 application/json 时,将使用ReceiveJson方法,但是如果将Content-Type header 设置为 application/xml 时,则将使用ReceiveXml方法。

总结

在本章中,我解释了 API 控制器在 MVC 应用程序中的作用。我演示了如何创建和测试 API 控制器,简要演示了如何使用 jQuery 生成异步 HTTP 请求,并解释了内容格式化过程。在下一章中,我将更详细地解释视图和视图引擎是如何工作的。

;

© 2018 - IOT小分队文章发布系统 v0.3